﻿using FFmpeg.AutoGen;
using System;
using System.Collections.Generic;

namespace FFmpegLib
{
    public sealed unsafe class FAVFormatContext : IDisposable
    {
        private static readonly AVRational TimeBaseQ = new AVRational { num = 1, den = ffmpeg.AV_TIME_BASE };

        static FAVFormatContext()
        {
            FFmpegLib.FFmpegUtils.FFmpegLibInit();
        }

        public string ErrInf { get; set; }

        public int VideoIndex { get; private set; } = -1;
        public int AudioIndex { get; private set; } = -1;
        public bool FmtCtxIsOpen { get; private set; }
        public bool IsInput { get; private set; }

        public AVFormatContext* FmtCtx;
        public AVCodecContext* VideoDeocder;
        public AVCodecContext* AudioDeocder;

        public AVCodecID VideoCodecID { get => Streams[VideoIndex].CodecID; }
        public AVCodecID AudioCodecID { get => Streams[AudioIndex].CodecID; }

        public Dictionary<int, FAVStream> Streams { get; private set; } = new Dictionary<int, FAVStream>();
        public FAVStream AudioStream { get => VideoIndex == -1 ? null : Streams[VideoIndex]; }
        public FAVStream VideoStream { get => AudioIndex == -1 ? null : Streams[AudioIndex]; }
        public double TotalSecs { get; private set; } = -1;

        /// <summary>
        /// 视频流第一帧pts
        /// </summary>
        public long VideoFirstPts;

        public FAVFormatContext() { }

        public FAVFormatContext(AVFormatContext* fmtCtx)
        {
            FmtCtx = fmtCtx;
        }

        public FAVFormatContext(IntPtr ptr)
        {
            FmtCtx = (AVFormatContext*)ptr;
        }

        public bool OpenFmtCtx(string url)
        {
            if (string.IsNullOrEmpty(url))
                throw new ArgumentNullException($"{nameof(url)}不能为空");

            if (FmtCtxIsOpen) { return true; }

            AVFormatContext* fmt_ctx = FmtCtx;
            if (fmt_ctx == null)
                fmt_ctx = ffmpeg.avformat_alloc_context();

            int ret = ffmpeg.avformat_open_input(&fmt_ctx, url, null, null);
            if (ret != 0) { goto error; }

            ret = ffmpeg.avformat_find_stream_info(fmt_ctx, null);
            if (ret != 0) { goto error; }

            if (fmt_ctx->duration != ffmpeg.AV_NOPTS_VALUE)
            {
                AVRational avr = fmt_ctx->streams[0]->time_base;
                AVRational rate = fmt_ctx->streams[0]->avg_frame_rate;
                TotalSecs = 1d * fmt_ctx->streams[0]->duration * avr.num / avr.den;
            }

            for (int i = 0; i < fmt_ctx->nb_streams; i++)
            {
                AVStream* stream = fmt_ctx->streams[i];
                AVCodecParameters* codecParam = stream->codecpar;
                if (codecParam->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
                {
                    VideoIndex = i;
                    FAVStream videoStream = new FAVStream(stream, TotalSecs);
                    VideoDeocder = videoStream.CodecCtx;
                    Streams.TryAdd((int)AVMediaType.AVMEDIA_TYPE_VIDEO, videoStream);
                }
                else if (codecParam->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO)
                {
                    AudioIndex = i;
                    FAVStream audioStream = new FAVStream(stream, TotalSecs);
                    AudioDeocder = audioStream.CodecCtx;
                    Streams.TryAdd((int)AVMediaType.AVMEDIA_TYPE_AUDIO, audioStream);
                }
            }

            FmtCtx = fmt_ctx;
            IsInput = true;
            return FmtCtxIsOpen = true;

        error:
            ErrInf = FFmpegUtils.GetErrorMessage(ret); // throw new ApplicationException(GetErrorMessage(error));
            ffmpeg.avformat_free_context(fmt_ctx);
            return false;
        }

        public bool CreateOutputFmtCtx(string url, string formatName,
            AVCodecParameters* videoCodecParam = null,
            AVCodecParameters* audioCodecParam = null)
        {
            if (string.IsNullOrEmpty(url))
                throw new ArgumentNullException($"{nameof(url)}不能为空");

            if (FmtCtxIsOpen) { return true; }

            int ret = 0;
            AVFormatContext* fmt_ctx = null;
            if (fmt_ctx == null)
                ret = ffmpeg.avformat_alloc_output_context2(&fmt_ctx, null, formatName, url);

            ret = ffmpeg.avio_open2(&fmt_ctx->pb, url, ffmpeg.AVIO_FLAG_READ_WRITE, null, null);
            if (ret != 0) { goto error; }

            if (videoCodecParam != null)
            {
                AVCodec* curCodec = ffmpeg.avcodec_find_decoder(videoCodecParam->codec_id);
                AVStream* stream = ffmpeg.avformat_new_stream(fmt_ctx, curCodec);
                ret = ffmpeg.avcodec_parameters_copy(stream->codecpar, videoCodecParam);
                stream->time_base = new AVRational { num = 1, den = ffmpeg.AV_TIME_BASE };
            }
            if (audioCodecParam != null)
            {
                AVCodec* curCodec = ffmpeg.avcodec_find_decoder(audioCodecParam->codec_id);
                AVStream* stream = ffmpeg.avformat_new_stream(fmt_ctx, curCodec);
                ret = ffmpeg.avcodec_parameters_copy(stream->codecpar, audioCodecParam);
                stream->time_base = new AVRational { num = 1, den = ffmpeg.AV_TIME_BASE };
            }
            ret = ffmpeg.avformat_write_header(fmt_ctx, null);
            if (ret < 0)
            { goto error; }

            FmtCtxIsOpen = true;
            IsInput = false;
            return FmtCtxIsOpen = true;

        error:
            ErrInf = FFmpegUtils.GetErrorMessage(ret); // throw new ApplicationException(GetErrorMessage(error));
            ffmpeg.avformat_free_context(fmt_ctx);
            return false;
        }

        public int ReadPacket(FAVPacket packet)
        {
            if (packet == null)
                throw new ArgumentNullException($"argumentName({nameof(packet)}) is null,is not allowed");
            lock (this)
                return ffmpeg.av_read_frame(FmtCtx, packet.Ptr);
        }

        public int Decode(FAVPacket inPacket, FAVFrame outFrame)
        {
            AVCodecContext* curCtx = null;
            if (inPacket.StreamIndex == VideoIndex)
            {
                curCtx = VideoDeocder;
                outFrame.MediaType = AVMediaType.AVMEDIA_TYPE_VIDEO;
            }
            else if (inPacket.StreamIndex == AudioIndex)
            {
                curCtx = AudioDeocder;
                outFrame.MediaType = AVMediaType.AVMEDIA_TYPE_AUDIO;
            }
            return DecodeAVPacket(curCtx, inPacket.Ptr, outFrame.Ptr);
        }

        /// <summary>
        /// 跳转到指定时间
        /// </summary>
        /// <param name="ms">毫秒</param>
        /// <returns></returns>
        public bool Seek(double seconds)
        {
            if (IsDisposed) { return false; }
            bool boolRet = true;
            lock (this)
            {
                long seekPos = GetPosFromTime(FmtCtx->streams[1]->time_base, seconds);
                int rs = ffmpeg.av_seek_frame(FmtCtx, -1, seekPos, ffmpeg.AVSEEK_FLAG_BACKWARD);
                boolRet = rs >= 0;

                //if (AudioStream != null)
                //{
                //    AudioStream.CurMsPos = (long)(seconds * 1000);
                //    long seekPos = GetPosFromTime(AudioStream.TimeBase, seconds);
                //    int rs = ffmpeg.av_seek_frame(FmtCtx, AudioIndex, seekPos, ffmpeg.AVSEEK_FLAG_BACKWARD);
                //    boolRet &= rs >= 0;
                //}
                //if (VideoStream != null)
                //{
                //    VideoStream.CurMsPos = (long)(seconds * 1000);
                //    long seekPos = GetPosFromTime(VideoStream.TimeBase, seconds);
                //    int rs = ffmpeg.av_seek_frame(FmtCtx, VideoIndex, seekPos, ffmpeg.AVSEEK_FLAG_BACKWARD);
                //    boolRet &= rs >= 0;
                //}
            }
            return boolRet;
        }



        #region dispose
        public bool IsDisposed => disposedValue;

        private bool disposedValue;
        private void Destroy()
        {
            lock (this)
            {
                if (VideoDeocder != null)
                {
                    ffmpeg.avcodec_close(VideoDeocder);
                    fixed (AVCodecContext** ptr = &VideoDeocder)
                        ffmpeg.avcodec_free_context(ptr);
                }
                if (AudioDeocder != null)
                {
                    ffmpeg.avcodec_close(AudioDeocder);
                    fixed (AVCodecContext** ptr = &AudioDeocder)
                        ffmpeg.avcodec_free_context(ptr);
                }
                if (FmtCtx != null)
                {
                    fixed (AVFormatContext** ptr = &FmtCtx)
                        ffmpeg.avformat_close_input(ptr);
                    ffmpeg.avformat_free_context(FmtCtx);
                    FmtCtx = null;
                }
            }
        }

        protected void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    Destroy();
                }
                disposedValue = true;
            }
        }

        public void Dispose()
        {
            // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
        #endregion

        #region static

        /// <summary>
        /// seek 时间转pts
        /// </summary>
        /// <param name="inStream"></param>
        /// <param name="seconds"></param>
        /// <returns></returns>
        public static long GetPosFromTime(AVRational inRational, double seconds)
        {
            long time = (long)(ffmpeg.AV_TIME_BASE * seconds);
            return ffmpeg.av_rescale_q(time, FAVFrame.TimeBaseQ, inRational);
        }

        public static int DecodeAVPacket(AVCodecContext* dec_ctx, AVPacket* in_packet, AVFrame* out_frame)
        {
            int ret = ffmpeg.avcodec_send_packet(dec_ctx, in_packet);
            if (ret < 0)
            {
                return ret;
            }
            // read all the output frames (in general there may be any number of them 
            return ffmpeg.avcodec_receive_frame(dec_ctx, out_frame);
        }
        #endregion
    }
}
